package com.vren.software.objectdetectionapi.service;

import com.vren.software.objectdetectionapi.domain.dto.Option;
import com.vren.software.objectdetectionapi.domain.vo.DetectionResult;
import com.vren.software.objectdetectionapi.domain.vo.ObjectPosition;
import com.vren.software.objectdetectionapi.domain.vo.PointPosition;
import lombok.extern.slf4j.Slf4j;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.*;
import org.opencv.features2d.*;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.xfeatures2d.SURF;
import org.springframework.stereotype.Component;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;

@Component
@Slf4j
public class FindContoursService {

    static {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    }

    private final static Double templateRealWidth = 25.0;

    public DetectionResult detection(Option option) throws Exception {
        if (option.getOffset() < 0) {
            throw new Exception("偏移量不能为负数");
        }

        Mat sourceMat = this.inputStream2Mat(option.getImages().getInputStream());
        List<Point[]> contours = this.findContours(sourceMat);
        final double ratio = (option.getRatio() == null || option.getRatio() <= 0) ? this.getRatio(contours) : option.getRatio();
        final double offset = option.getOffset();
        option.setRatio(ratio);
        DetectionResult detectionResult = new DetectionResult();
        detectionResult.setOption(option);
        ArrayList<ObjectPosition> objectPositions = new ArrayList<>();
        int height = sourceMat.height();
        int width = sourceMat.width();
        int thickness = 2;
        int font = Imgproc.FONT_HERSHEY_PLAIN;
        double fontScale = Math.min(width, height) / 400;
        for (int i = 0; i < contours.size(); i++) {
            Point[] points = contours.get(i);
            Double objectWidth = this.keep2decimal(this.getLengthByPythagoreanTheorem(points[0], points[1]) / ratio);
            Double objectHeight = this.keep2decimal(this.getLengthByPythagoreanTheorem(points[3], points[0]) / ratio);
            ObjectPosition objectPosition = new ObjectPosition();
            objectPosition.setHeight(objectHeight);
            objectPosition.setWidth(objectWidth);
            PointPosition pointPosition = new PointPosition();
            pointPosition.setCoordinateX(this.keep2decimal(points[4].x / ratio) + offset);
            pointPosition.setCoordinateY(this.keep2decimal(points[4].y / ratio));
            objectPosition.setCenter(pointPosition);
            objectPositions.add(objectPosition);
            for (int j = 0; j < points.length; j++) {
                Imgproc.line(sourceMat, points[j % 4], points[(j + 1) % 4], new Scalar(0, 0, 255), 4);
            }
            String widthFormat = String.format("%.2fmm", objectWidth);
            Size w = Imgproc.getTextSize(widthFormat, font, fontScale, thickness, null);
            Imgproc.putText(sourceMat, widthFormat,
                    new Point(
                            ((points[1].x - points[0].x) / 2 + points[0].x) - (w.width / 2),
                            (points[0].y - points[1].y) / 2 + points[1].y
                    ),
                    font, fontScale, new Scalar(0, 0, 255), 4, Imgproc.LINE_4, false);

            String heightFormat = String.format("%.2fmm", objectHeight);
            Size h = Imgproc.getTextSize(heightFormat, font, fontScale, thickness, null);
            Imgproc.putText(sourceMat, String.format("%.2fmm", objectHeight),
                    new Point(
                            ((points[3].x - points[0].x) / 2 + points[0].x) - (h.width / 2),
                            (points[0].y - points[3].y) / 2 + points[3].y
                    ),
                    font, fontScale, new Scalar(0, 0, 255), 4, Imgproc.LINE_4, false);
        }
        MatOfByte matOfByte = new MatOfByte();
        Imgcodecs.imencode(".png", sourceMat, matOfByte);
        detectionResult.setImages(matOfByte.toArray());
        detectionResult.setObjectList(objectPositions);
        return detectionResult;
    }

    private Mat inputStream2Mat(InputStream inputStream) throws IOException {
        BufferedInputStream bis = new BufferedInputStream(inputStream);
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int bytesRead = 0;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
        os.flush();
        os.close();
        inputStream.close();
        Mat encoded = new Mat(1, os.size(), 0);
        encoded.put(0, 0, os.toByteArray());
        Mat decoded = Imgcodecs.imdecode(encoded, -1);
        encoded.release();
        return decoded;
    }

    private Double keep2decimal(Double value) {
        return Double.parseDouble(String.format("%.2f", value));
    }

    private void printMat(Mat src) {
        for (int i = 0; i < src.rows(); i++) {
            for (int i1 = 0; i1 < src.cols(); i1++) {
                System.out.println(Arrays.toString(src.get(i, i1)));
            }
        }
        System.out.println();
    }

    private List<Point[]> findContours(Mat sourceMat) {
        if (sourceMat.height() > sourceMat.width()) {
            //行比列多 说明是竖图  需要旋转  以长边为宽
            Core.rotate(sourceMat, sourceMat, Core.ROTATE_90_COUNTERCLOCKWISE);
        }
        Mat binary = new Mat();
        Imgproc.cvtColor(sourceMat, binary, Imgproc.COLOR_BGR2GRAY);
        Imgproc.GaussianBlur(binary, binary, new Size(5, 5), 0);
        Imgproc.Canny(binary, binary, 60, 230);
        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_OPEN, new Size(80, 80));
        Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_CLOSE, kernel);
        List<MatOfPoint> contours = new ArrayList<>();
        Imgproc.findContours(binary, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE);
        List<Point[]> list = new ArrayList<>();
        for (int i = 0; i < contours.size(); i++) {
            RotatedRect rotatedRect = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(i).toArray()));
            Point points[] = new Point[5];
            rotatedRect.points(points);
            points[4] = rotatedRect.center;
            list.add(points);
        }
        return list;
    }

    public Mat test(String path) {
        Mat sourceMat = Imgcodecs.imread(path);
        if (sourceMat.height() > sourceMat.width()) {
            //行比列多 说明是竖图  需要旋转  以长边为宽
            Core.rotate(sourceMat, sourceMat, Core.ROTATE_90_COUNTERCLOCKWISE);
        }
        Mat binary = new Mat();
        Imgproc.cvtColor(sourceMat, binary, Imgproc.COLOR_BGR2GRAY);
        Imgproc.GaussianBlur(binary, binary, new Size(5, 5), 0);
        Imgproc.Canny(binary, binary, 60, 230);
        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_OPEN, new Size(80, 80));
        Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_CLOSE, kernel);
        List<MatOfPoint> contours = new ArrayList<>();
        Imgproc.findContours(binary, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE);
        for (int i = 0; i < contours.size(); i++) {
            Rect rect = Imgproc.boundingRect(contours.get(i));
            Imgproc.rectangle(sourceMat, rect, new Scalar(0, 0, 255), 4);
        }
        return sourceMat;
    }

    public Mat find(String path) {
        Mat sourceMat = Imgcodecs.imread(path);
        List<Point[]> contours = this.findContours(sourceMat);
        double ratio = this.getRatio(contours);
        int height = sourceMat.height();
        int width = sourceMat.width();
        int thickness = 2;
        int font = Imgproc.FONT_HERSHEY_PLAIN;
        double fontScale = Math.min(width, height) / 400;
        for (int i = 0; i < contours.size(); i++) {
            Point[] points = contours.get(i);
            Double objectWidth = this.keep2decimal(this.getLengthByPythagoreanTheorem(points[0], points[1]) / ratio);
            Double objectHeight = this.keep2decimal(this.getLengthByPythagoreanTheorem(points[3], points[0]) / ratio);
            for (int j = 0; j < points.length; j++) {
                Imgproc.line(sourceMat, points[j % 4], points[(j + 1) % 4], new Scalar(0, 255, 0), 4);
            }
            String widthFormat = String.format("%.2fmm", objectWidth);
            Size w = Imgproc.getTextSize(widthFormat, font, fontScale, thickness, null);
            Imgproc.putText(sourceMat, widthFormat,
                    new Point(
                            ((points[1].x - points[0].x) / 2 + points[0].x) - (w.width / 2),
                            (points[0].y - points[1].y) / 2 + points[1].y
                    ),
                    font, fontScale, new Scalar(0, 0, 255), 4, Imgproc.LINE_4, false);

            String heightFormat = String.format("%.2fmm", objectHeight);
            Size h = Imgproc.getTextSize(heightFormat, font, fontScale, thickness, null);
            Imgproc.putText(sourceMat, String.format("%.2fmm", objectHeight),
                    new Point(
                            ((points[3].x - points[0].x) / 2 + points[0].x) - (h.width / 2),
                            (points[0].y - points[3].y) / 2 + points[3].y
                    ),
                    font, fontScale, new Scalar(0, 0, 255), 4, Imgproc.LINE_4, false);
        }
        return sourceMat;
    }


    private Double getRatio(List<Point[]> list) {
        int index = -1;
        double minX = 0;
        for (int i = 0; i < list.size(); i++) {
            Point temp = Arrays.stream(list.get(i)).sorted(Comparator.comparing(item -> item.x)).collect(Collectors.toList()).get(0);
            if (index == -1 || temp.x <= minX) {
                index = i;
                minX = temp.x;
            }
        }
        Point[] leftMost = list.get(index);
        double width = leftMost[1].x - leftMost[0].x;
        return width / templateRealWidth;
    }

    private Double getLengthByPythagoreanTheorem(Point a, Point b) {
        double x = Math.abs(a.x - b.x);
        double y = Math.abs(a.y - b.y);
        if (x == 0.0) {
            return y;
        }
        if (y == 0.0) {
            return x;
        }
        return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
    }


}
